//
//  ZODCacheManager.m
//  ZTEOnlineDoc
//
//  Created by 万伟琦 on 2018/9/11.
//  Copyright © 2018年 ZTE. All rights reserved.
//

#import "ZODCacheManager.h"
#import "NSFileManager+ZOD.h"
#import "ZODDocCacheInfo.h"
#import "ZODInternalTools.h"
#import "ZODGlobalStorage.h"

#define MAX_CACHE_SIZE 1024 * 100
#define MAX_CACHE_DAYS 7

@interface ZODCacheManager ()


/**
 单位为 KB
 */
@property (nonatomic, assign) NSUInteger maxCacheSize; // 1024 * 100  (100M)

@property (nonatomic, strong) NSMutableDictionary *chunkDataPair;
@property (nonatomic, assign) NSTimeInterval expiredTimeInterval;

// TODO: 这两个锁有待商榷
@property (nonatomic, strong) dispatch_semaphore_t pageContentLock;
@property (nonatomic, strong) dispatch_semaphore_t writeFileLock;


//@property (nonatomic, strong) dispatch_queue_t cacheInfoQueue;

@end

@implementation ZODCacheManager


//- (dispatch_queue_t)cacheInfoQueue {
//    if (!_cacheInfoQueue) {
//        _cacheInfoQueue = dispatch_queue_create("cache info queue", DISPATCH_QUEUE_SERIAL);
//    }
//    return _cacheInfoQueue;
//}

+ (instancetype)sharedInstance {
    static ZODCacheManager *_instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[ZODCacheManager alloc] init];
        _instance.cacheInfos = [NSMutableDictionary dictionary];
        _instance.maxCacheSize = MAX_CACHE_SIZE;
//        _instance.maxCacheSize = 1024 * 10;
        _instance.expiredTimeInterval = -60 * 60 * 24 * MAX_CACHE_DAYS;
        _instance.pageContentLock = dispatch_semaphore_create(1);
        _instance.writeFileLock = dispatch_semaphore_create(1);
    });
    return _instance;
}

- (void)cacheImageInMemoryWithDocID:(NSString *)docMD5
                            authRes:(NSString *)authRes
                         totalPages:(NSUInteger)totalPages
                           currPage:(NSInteger)page
                    imgBase64Length:(NSUInteger)base64Len
                             chunks:(NSUInteger)chunks
                          currChunk:(NSUInteger)chunk
                          chunkData:(NSString *)chunkString
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        

        @synchronized (self) {
            NSString *fileMD5 = docMD5;
            NSString *pageStr = [NSString stringWithFormat:@"%ld", (long)page];
            ZODDocCacheInfo *cacheInfo = [self.cacheInfos objectForKey:fileMD5];

            //刚打开这个文件准备进行缓存（这个文件可能之前已经在沙盒中有缓存）
            if (!cacheInfo) {
                cacheInfo = [[ZODDocCacheInfo alloc] init];
                cacheInfo.docID = fileMD5;
                cacheInfo.totalPages = [NSString stringWithFormat:@"%lu",(unsigned long)totalPages];
                cacheInfo.authRes = authRes;

                //检查 index 文件
                NSURL *indexFileURL = [NSDefaultFileManager getDocCacheIndexFileURLByDocMD5:fileMD5];

                //读取 index 文件，看是否有 json 字符串。若有，则说明以前缓存过这个文件
                NSData *indexFileData = [NSData dataWithContentsOfURL:indexFileURL];
//                if (!indexFileData) {
//                    return NO;
//                }

                if (indexFileData.length < 1) { //空文件
                    cacheInfo.cachedPages = [NSMutableArray arrayWithObject:pageStr];
                }else { //以前有缓存过
                    NSDictionary *indexDict = [NSJSONSerialization JSONObjectWithData:indexFileData options:NSJSONReadingAllowFragments error:NULL];
                    NSArray *cPages = indexDict[@"cachedPages"];
                    if (cPages) {
                        cacheInfo.cachedPages = [NSMutableArray arrayWithArray:cPages];

                        if (![cacheInfo.cachedPages containsObject:pageStr]) {
                            [cacheInfo.cachedPages addObject:pageStr];
                        }
                    }else {
//                        return NO;
                    }
                }

            }else {
                if (![cacheInfo.cachedPages containsObject:pageStr]) {
                    [cacheInfo.cachedPages addObject:pageStr];
                }
            }

            // 还需要处理 chunks
            NSMutableDictionary *chunkDataPair = self.chunkDataPair;
            [chunkDataPair setObject:chunkString forKey:[NSNumber numberWithUnsignedInteger:chunk]];

            // 如果 key 的数量达到了 chunks，那么开始组合字典里面的数据
            if (chunkDataPair.allKeys.count == chunks) {
                NSSet *keysSet = [NSSet setWithArray:chunkDataPair.allKeys];
                if (keysSet.count != chunkDataPair.allKeys.count) {
                    NSLog(@"---------存在重复的 chunk ！ keysSet：%lu，chunkDataPair.allKeys：%ld", (unsigned long)keysSet.count, chunkDataPair.allKeys.count);
                    return;
                }
                NSArray *allKeys = [chunkDataPair.allKeys sortedArrayUsingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
                    return [obj1 compare:obj2];
                }];

                NSMutableString *imageDataString = [NSMutableString string];
                for (NSNumber *chunkKey in allKeys) {
                    NSString *chunkDataStr = [chunkDataPair objectForKey:chunkKey];
                    [imageDataString appendString:chunkDataStr];
                }
                NSData *data = [[NSData alloc] initWithBase64EncodedString:imageDataString options:NSDataBase64DecodingIgnoreUnknownCharacters];
                
//                NSLog(@"chunkDataLen: %ld", data.length);
                if (data.length < 15000) {
                    NSLog(@"base64Len = %lu", (unsigned long)base64Len);
                }
                
                dispatch_semaphore_wait(self.pageContentLock, DISPATCH_TIME_FOREVER);
                [cacheInfo.pageContents setObject:data forKey:pageStr];
                dispatch_semaphore_signal(self.pageContentLock);
//                @synchronized (self) {
                    [self.cacheInfos setObject:cacheInfo forKey:fileMD5];
//                }
                [self.chunkDataPair removeAllObjects];
            }
            
            // 及时将内存中的缓存写入磁盘，防止爆内存，5 页一写
            // FIXME: 这里异步写入磁盘再回调 remove 元素有问题，会导致 docCache.cachedPages.count 的值不符合预期
            // TODO: 这是生产者消费者模式，内存数据为生产者，磁盘写入为消费者。看看能不能用条件锁
            /*
             问题场景：
             比如我当前 count 等于 5 页，然后开始写入磁盘，由于是异步写入，所以回调的 remove 会晚一点调用
             然后这个时候又开始 cache，cachedPage 会自增至 6，然后又开始磁盘写入
             这样就会导致已经写入磁盘的前 5 页数据重复写入，造成性能浪费
             */
            ZODDocCacheInfo *docCache = [self.cacheInfos objectForKey:fileMD5];
            if (docCache.cachedPages.count > 4) {
                [self writeMemoryCacheToDiskWithCompletion:^{
//                    dispatch_semaphore_wait(self.pageContentLock, DISPATCH_TIME_FOREVER);
                    [docCache.pageContents removeAllObjects];
//                    dispatch_semaphore_signal(self.pageContentLock);
                }];
            }

        }
    });

//    return YES;
}

- (void)cacheAuthResult:(NSString *)authResult {
    
    // 在同一时间只预览一个文件的情况下，这个循环只会走一次
    for (NSString *fileMD5 in self.cacheInfos.allKeys) {
        ZODDocCacheInfo *cacheInfo;
        @synchronized (self) {
             cacheInfo = [self.cacheInfos objectForKey:fileMD5];
        }
        if (!cacheInfo) {
            continue;
        }
        NSURL *indexFileURL = [NSDefaultFileManager getDocCacheIndexFileURLByDocMD5:fileMD5];
        cacheInfo.authRes = authResult;
        NSDictionary *jsonDict = @{
                                   @"docID": cacheInfo.docID,
                                   @"authRes":cacheInfo.authRes,
                                   @"totalPages": cacheInfo.totalPages,
                                   @"cachedPages": cacheInfo.cachedPages,
                                   };
        NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonDict options:NSJSONWritingPrettyPrinted error:NULL];
        if ([jsonData isKindOfClass:[NSData class]]) {
            dispatch_semaphore_wait(self.writeFileLock, DISPATCH_TIME_FOREVER);
            [jsonData writeToURL:indexFileURL atomically:YES];
            dispatch_semaphore_signal(self.writeFileLock);
        }
    }
}

- (void)writeMemoryCacheToDiskWithCompletion:(void (^)(void))completion {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        for (NSString *fileMD5 in self.cacheInfos.allKeys) {
            ZODDocCacheInfo *cacheInfo = [self.cacheInfos objectForKey:fileMD5];
            // 写 index 文件
            NSURL *indexFileURL = [NSDefaultFileManager getDocCacheIndexFileURLByDocMD5:fileMD5];
            NSDictionary *jsonDict = @{
                                       @"docID": cacheInfo.docID,
                                       @"authRes":cacheInfo.authRes,
                                       @"totalPages": cacheInfo.totalPages,
                                       @"cachedPages": cacheInfo.cachedPages,
                                       };
            NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonDict options:NSJSONWritingPrettyPrinted error:NULL];
            if ([jsonData isKindOfClass:[NSData class]]) {
                dispatch_semaphore_wait(self.writeFileLock, DISPATCH_TIME_FOREVER);
                [jsonData writeToURL:indexFileURL atomically:YES];
                dispatch_semaphore_signal(self.writeFileLock);
            }
            // 写 page 文件
//            NSLog(@"pageContents count: %ld", cacheInfo.pageContents.count);
            [cacheInfo.pageContents enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSData *data, BOOL * _Nonnull stop) {
                NSString *keyPathComponent = key;
                NSURL *pageURL = [[NSDefaultFileManager getDocumentDirectoryURLByDocMD5:fileMD5] URLByAppendingPathComponent:keyPathComponent];
                @synchronized ([ZODCacheManager class]) {
                    
                    BOOL isPageExisted = [NSDefaultFileManager fileExistsAtPath:pageURL.path];
                    if (!isPageExisted) {
                        [NSDefaultFileManager createFileAtPath:pageURL.path contents:data attributes:nil];
                        NSLog(@"写入第 %@ 页, 数据长度: %lu", key, data.length);
                    }
                }
            }];
            
            [self.cacheInfos removeObjectForKey:fileMD5];
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            !completion?:completion();
        });
    });
}


- (NSString *)getLocalCachesJSONStringByFileMD5:(NSString *)fileMD5 {
    //获取 index 里面的 json 信息
    NSURL *indexFileURL = [NSDefaultFileManager getDocCacheIndexFileURLByDocMD5:fileMD5];
    NSString *jsonStr = [NSString stringWithContentsOfURL:indexFileURL encoding:NSUTF8StringEncoding error:NULL];
    if (!jsonStr || jsonStr.length < 1) {
        return nil;
    }
    
    NSDictionary *jsonDict = [ZODInternalTools dictionaryFromJSONString:jsonStr];
    NSArray *pages = jsonDict[@"cachedPages"];
    NSString *totalPages = jsonDict[@"totalPages"];
    
    NSMutableDictionary *pagesDict = [NSMutableDictionary dictionary];
    NSString *uno = [ZODGlobalStorage sharedStorage].configuration.userNo;
    for (NSString *page in pages) {
        //使用 GCDWebServer 的地址
        NSString *pageURLString = [NSString stringWithFormat:@"http://localhost:8007/%@/%@/%@",uno,fileMD5,page];
        [pagesDict setObject:pageURLString forKey:page];
    }
    NSDictionary *retDict = @{
                               @"totalPages": totalPages,
                               @"pages": pagesDict,
                               };
    
    NSString *ret = [ZODInternalTools JSONStringFromDictionary:retDict];
    return ret;
}


- (void)clearFileCachesByRule {
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        [self clearAllCachesExceptCurrentAccount:^{
            
            NSURL *cacheURLByAccount = [NSDefaultFileManager getCacheDirectoryURLByCurrentAccount];
            NSArray *resKeys = @[NSURLContentAccessDateKey, NSURLTotalFileAllocatedSizeKey, NSURLTotalFileSizeKey, NSURLFileSizeKey, NSURLFileAllocatedSizeKey];
            
            NSArray *fileURLs = [NSDefaultFileManager contentsOfDirectoryAtURL:cacheURLByAccount includingPropertiesForKeys:resKeys options:NSDirectoryEnumerationSkipsSubdirectoryDescendants error:NULL];
            NSDate *expireDate = [NSDate dateWithTimeIntervalSinceNow:self.expiredTimeInterval];
            
            NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];     //key：URL, value：dict。 每个 dict 对应一个文档 resource 属性
            NSMutableArray *urlsToDelete = [NSMutableArray array];                  //记录需要删除的文档 URL
            NSMutableDictionary *docSizeDict = [NSMutableDictionary dictionary];    //临时缓存，用于记录每个文档对应的缓存大小
            NSUInteger currentCacheSize = 0;
            
            for (NSURL *docDirURL in fileURLs) {
                NSDictionary *resValues = [docDirURL resourceValuesForKeys:resKeys error:NULL];
                NSDate *lastAccessDate = resValues[NSURLContentAccessDateKey];
                
                // 超过规定时间开始清理
                if ([[lastAccessDate laterDate:expireDate] isEqualToDate:expireDate]) {
                    [urlsToDelete addObject:docDirURL];
                    continue;
                }
                
                // 计算不需要根据时间删除的文件 size
                NSUInteger docDirSize = [NSDefaultFileManager getFileSizeAtPath:docDirURL.path];
                currentCacheSize += docDirSize;
                
                [docSizeDict setObject:[NSNumber numberWithUnsignedInteger:docDirSize] forKey:docDirURL.absoluteString];
                [cacheFiles setObject:resValues forKey:docDirURL];
            }
            
            for (NSURL *dirURL in urlsToDelete) {
                [NSDefaultFileManager removeItemAtURL:dirURL error:NULL];
            }
            
            // 经过时间筛选后，超过设定 maxCacheSize 开始清理
            if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
                
                const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
                
                // 将剩余的缓存文件按照上次访问时间进行排序（oldest first）
                NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent usingComparator:^NSComparisonResult(id obj1, id obj2) {
                    return [obj1[NSURLContentAccessDateKey] compare:obj2[NSURLContentAccessDateKey]];
                }];
                // 删除文件直到缓存文件总大小小于规定 size
                for (NSURL *docURL in sortedFiles) {
                    
                    //取临时缓存中文档对应的缓存
                    //NSUInteger fileSize = [NSDefaultFileManager getFileSizeAtPath:fileURL.path];
                    id val = [docSizeDict objectForKey:docURL.absoluteString];
                    NSUInteger fileSize = 0;
                    if ([val isKindOfClass:[NSNumber class]]) {
                        NSNumber *fileSizeNumber = (NSNumber *)val;
                        fileSize = fileSizeNumber.unsignedIntegerValue;
                    }
                    
                    if ([NSDefaultFileManager removeItemAtURL:docURL error:nil]) {
                        currentCacheSize -= fileSize;
                        
                        if (currentCacheSize < desiredCacheSize) {
                            break;
                        }
                    }
                }
            }
        }];
        
    });
}

- (void)clearAllCachesExceptCurrentAccount:(void(^)(void))completion {
    NSURL *allCacheURL = [NSDefaultFileManager getSDKRootCacheDirectoryURL];
    NSArray *accountDirsURLs = [NSDefaultFileManager contentsOfDirectoryAtURL:allCacheURL includingPropertiesForKeys:@[] options:NSDirectoryEnumerationSkipsSubdirectoryDescendants error:NULL];
    NSString *uno = [ZODGlobalStorage sharedStorage].configuration.userNo;
    for (NSURL *accountCacheDir in accountDirsURLs) {
        NSString *lastComponent = accountCacheDir.lastPathComponent;
        if (![lastComponent isEqualToString:uno]) {
            [NSDefaultFileManager removeItemAtURL:accountCacheDir error:NULL];
        }
    }
    !completion?:completion();
}

- (BOOL)clearAllCaches {
    NSURL *sdkCacheURL = [NSDefaultFileManager getSDKRootCacheDirectoryURL];
    if (sdkCacheURL) {
       return [NSDefaultFileManager removeItemAtURL:sdkCacheURL error:NULL];
    }
    return NO;
}

#pragma mark - Lazy load

- (NSMutableDictionary *)chunkDataPair {
    if (!_chunkDataPair) {
        _chunkDataPair = [NSMutableDictionary dictionary];
    }
    return _chunkDataPair;
}

@end
